/*
 * Copyright (c) 2008-2010, Matthias Mann
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Matthias Mann nor the names of its contributors may
 *       be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *     * Redistributions in source or binary form must keep the original package
 *       and class name.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package de.matthiasmann.twl.utils;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

/**
 * A PNGDecoder. The slick PNG decoder is based on this class :)
 *
 * @author Matthias Mann
 */
public class PNGDecoder {

    public enum Format {
        ALPHA(1, true),
        LUMINANCE(1, false),
        LUMINANCE_ALPHA(2, true),
        RGB(3, false),
        RGBA(4, true),
        BGRA(4, true),
        ABGR(4, true);

        final int numComponents;
        final boolean hasAlpha;

        private Format(int numComponents, boolean hasAlpha) {
            this.numComponents = numComponents;
            this.hasAlpha = hasAlpha;
        }

        public int getNumComponents() {
            return numComponents;
        }

        public boolean isHasAlpha() {
            return hasAlpha;
        }
    }

    private static final byte[] SIGNATURE = {(byte)137, 80, 78, 71, 13, 10, 26, 10};

    private static final int IHDR = 0x49484452;
    private static final int PLTE = 0x504C5445;
    private static final int tRNS = 0x74524E53;
    private static final int IDAT = 0x49444154;
    private static final int IEND = 0x49454E44;

    private static final byte COLOR_GREYSCALE = 0;
    private static final byte COLOR_TRUECOLOR = 2;
    private static final byte COLOR_INDEXED = 3;
    private static final byte COLOR_GREYALPHA = 4;
    private static final byte COLOR_TRUEALPHA = 6;

    private final InputStream input;
    private final CRC32 crc;
    private final byte[] buffer;

    private int chunkLength;
    private int chunkType;
    private int chunkRemaining;

    private int width;
    private int height;
    private int bitdepth;
    private int colorType;
    private int bytesPerPixel;
    private byte[] palette;
    private byte[] paletteA;
    private byte[] transPixel;

    public PNGDecoder(InputStream input) throws IOException {
        this.input = input;
        this.crc = new CRC32();
        this.buffer = new byte[4096];

        readFully(buffer, 0, SIGNATURE.length);
        if(!checkSignature(buffer)) {
            throw new IOException("Not a valid PNG file");
        }

        openChunk(IHDR);
        readIHDR();
        closeChunk();

        searchIDAT: for(;;) {
            openChunk();
            switch (chunkType) {
                case IDAT:
                    break searchIDAT;
                case PLTE:
                    readPLTE();
                    break;
                case tRNS:
                    readtRNS();
                    break;
            }
            closeChunk();
        }

        if(colorType == COLOR_INDEXED && palette == null) {
            throw new IOException("Missing PLTE chunk");
        }
    }

    public int getHeight() {
        return height;
    }

    public int getWidth() {
        return width;
    }

    /**
     * Checks if the image has a real alpha channel.
     * This method does not check for the presence of a tRNS chunk.
     *
     * @return true if the image has an alpha channel
     * @see #hasAlpha()
     */
    public boolean hasAlphaChannel() {
        return colorType == COLOR_TRUEALPHA || colorType == COLOR_GREYALPHA;
    }

    /**
     * Checks if the image has transparency information either from
     * an alpha channel or from a tRNS chunk.
     *
     * @return true if the image has transparency
     * @see #hasAlphaChannel()
     * @see #overwriteTRNS(byte, byte, byte)
     */
    public boolean hasAlpha() {
        return hasAlphaChannel() ||
                paletteA != null || transPixel != null;
    }

    public boolean isRGB() {
        return colorType == COLOR_TRUEALPHA ||
                colorType == COLOR_TRUECOLOR ||
                colorType == COLOR_INDEXED;
    }

    /**
     * Overwrites the tRNS chunk entry to make a selected color transparent.
     * <p>This can only be invoked when the image has no alpha channel.</p>
     * <p>Calling this method causes {@link #hasAlpha()} to return true.</p>
     *
     * @param r the red component of the color to make transparent
     * @param g the green component of the color to make transparent
     * @param b the blue component of the color to make transparent
     * @throws UnsupportedOperationException if the tRNS chunk data can't be set
     * @see #hasAlphaChannel()
     */
    public void overwriteTRNS(byte r, byte g, byte b) {
        if(hasAlphaChannel()) {
            throw new UnsupportedOperationException("image has an alpha channel");
        }
        byte[] pal = this.palette;
        if(pal == null) {
            transPixel = new byte[] { 0, r, 0, g, 0, b };
        } else {
            paletteA = new byte[pal.length/3];
            for(int i=0,j=0 ; i<pal.length ; i+=3,j++) {
                if(pal[i] != r || pal[i+1] != g || pal[i+2] != b) {
                    paletteA[j] = (byte)0xFF;
                }
            }
        }
    }

    /**
     * Computes the implemented format conversion for the desired format.
     *
     * @param fmt the desired format
     * @return format which best matches the desired format
     * @throws UnsupportedOperationException if this PNG file can't be decoded
     */
    public Format decideTextureFormat(Format fmt) {
        switch (colorType) {
            case COLOR_TRUECOLOR:
                switch (fmt) {
                    case ABGR:
                    case RGBA:
                    case BGRA:
                    case RGB: return fmt;
                    default: return Format.RGB;
                }
            case COLOR_TRUEALPHA:
                switch (fmt) {
                    case ABGR:
                    case RGBA:
                    case BGRA:
                    case RGB: return fmt;
                    default: return Format.RGBA;
                }
            case COLOR_GREYSCALE:
                switch (fmt) {
                    case LUMINANCE:
                    case ALPHA: return fmt;
                    default: return Format.LUMINANCE;
                }
            case COLOR_GREYALPHA:
                return Format.LUMINANCE_ALPHA;
            case COLOR_INDEXED:
                switch (fmt) {
                    case ABGR:
                    case RGBA:
                    case BGRA: return fmt;
                    default: return Format.RGBA;
                }
            default:
                throw new UnsupportedOperationException("Not yet implemented");
        }
    }

    /**
     * Decodes the image into the specified buffer. The first line is placed at
     * the current position. After decode the buffer position is at the end of
     * the last line.
     *
     * @param buffer the buffer
     * @param stride the stride in bytes from start of a line to start of the next line, can be negative.
     * @param fmt the target format into which the image should be decoded.
     * @throws IOException if a read or data error occurred
     * @throws IllegalArgumentException if the start position of a line falls outside the buffer
     * @throws UnsupportedOperationException if the image can't be decoded into the desired format
     */
    public void decode(ByteBuffer buffer, int stride, Format fmt) throws IOException {
        final int offset = buffer.position();
        final int lineSize = ((width * bitdepth + 7) / 8) * bytesPerPixel;
        byte[] curLine = new byte[lineSize+1];
        byte[] prevLine = new byte[lineSize+1];
        byte[] palLine = (bitdepth < 8) ? new byte[width+1] : null;

        final Inflater inflater = new Inflater();
        try {
            for(int y=0 ; y<height ; y++) {
                readChunkUnzip(inflater, curLine, 0, curLine.length);
                unfilter(curLine, prevLine);

                buffer.position(offset + y*stride);

                switch (colorType) {
                    case COLOR_TRUECOLOR:
                        switch (fmt) {
                            case ABGR: copyRGBtoABGR(buffer, curLine); break;
                            case RGBA: copyRGBtoRGBA(buffer, curLine); break;
                            case BGRA: copyRGBtoBGRA(buffer, curLine); break;
                            case RGB: copy(buffer, curLine); break;
                            default: throw new UnsupportedOperationException("Unsupported format for this image");
                        }
                        break;
                    case COLOR_TRUEALPHA:
                        switch (fmt) {
                            case ABGR: copyRGBAtoABGR(buffer, curLine); break;
                            case RGBA: copy(buffer, curLine); break;
                            case BGRA: copyRGBAtoBGRA(buffer, curLine); break;
                            case RGB: copyRGBAtoRGB(buffer, curLine); break;
                            default: throw new UnsupportedOperationException("Unsupported format for this image");
                        }
                        break;
                    case COLOR_GREYSCALE:
                        switch (fmt) {
                            case LUMINANCE:
                            case ALPHA: copy(buffer, curLine); break;
                            default: throw new UnsupportedOperationException("Unsupported format for this image");
                        }
                        break;
                    case COLOR_GREYALPHA:
                        switch (fmt) {
                            case LUMINANCE_ALPHA: copy(buffer, curLine); break;
                            default: throw new UnsupportedOperationException("Unsupported format for this image");
                        }
                        break;
                    case COLOR_INDEXED:
                        switch(bitdepth) {
                            case 8: palLine = curLine; break;
                            case 4: expand4(curLine, palLine); break;
                            case 2: expand2(curLine, palLine); break;
                            case 1: expand1(curLine, palLine); break;
                            default: throw new UnsupportedOperationException("Unsupported bitdepth for this image");
                        }
                        switch (fmt) {
                            case ABGR: copyPALtoABGR(buffer, palLine); break;
                            case RGBA: copyPALtoRGBA(buffer, palLine); break;
                            case BGRA: copyPALtoBGRA(buffer, palLine); break;
                            default: throw new UnsupportedOperationException("Unsupported format for this image");
                        }
                        break;
                    default:
                        throw new UnsupportedOperationException("Not yet implemented");
                }

                byte[] tmp = curLine;
                curLine = prevLine;
                prevLine = tmp;
            }
        } finally {
            inflater.end();
        }
    }

    /**
     * Decodes the image into the specified buffer. The last line is placed at
     * the current position. After decode the buffer position is at the end of
     * the first line.
     *
     * @param buffer the buffer
     * @param stride the stride in bytes from start of a line to start of the next line, must be positive.
     * @param fmt the target format into which the image should be decoded.
     * @throws IOException if a read or data error occurred
     * @throws IllegalArgumentException if the start position of a line falls outside the buffer
     * @throws UnsupportedOperationException if the image can't be decoded into the desired format
     */
    public void decodeFlipped(ByteBuffer buffer, int stride, Format fmt) throws IOException {
        if(stride <= 0) {
            throw new IllegalArgumentException("stride");
        }
        int pos = buffer.position();
        int posDelta = (height-1) * stride;
        buffer.position(pos + posDelta);
        decode(buffer, -stride, fmt);
        buffer.position(buffer.position() + posDelta);
    }

    private void copy(ByteBuffer buffer, byte[] curLine) {
        buffer.put(curLine, 1, curLine.length-1);
    }

    private void copyRGBtoABGR(ByteBuffer buffer, byte[] curLine) {
        if(transPixel != null) {
            byte tr = transPixel[1];
            byte tg = transPixel[3];
            byte tb = transPixel[5];
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                byte r = curLine[i];
                byte g = curLine[i+1];
                byte b = curLine[i+2];
                byte a = (byte)0xFF;
                if(r==tr && g==tg && b==tb) {
                    a = 0;
                }
                buffer.put(a).put(b).put(g).put(r);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                buffer.put((byte)0xFF).put(curLine[i+2]).put(curLine[i+1]).put(curLine[i]);
            }
        }
    }

    private void copyRGBtoRGBA(ByteBuffer buffer, byte[] curLine) {
        if(transPixel != null) {
            byte tr = transPixel[1];
            byte tg = transPixel[3];
            byte tb = transPixel[5];
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                byte r = curLine[i];
                byte g = curLine[i+1];
                byte b = curLine[i+2];
                byte a = (byte)0xFF;
                if(r==tr && g==tg && b==tb) {
                    a = 0;
                }
                buffer.put(r).put(g).put(b).put(a);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                buffer.put(curLine[i]).put(curLine[i+1]).put(curLine[i+2]).put((byte)0xFF);
            }
        }
    }

    private void copyRGBtoBGRA(ByteBuffer buffer, byte[] curLine) {
        if(transPixel != null) {
            byte tr = transPixel[1];
            byte tg = transPixel[3];
            byte tb = transPixel[5];
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                byte r = curLine[i];
                byte g = curLine[i+1];
                byte b = curLine[i+2];
                byte a = (byte)0xFF;
                if(r==tr && g==tg && b==tb) {
                    a = 0;
                }
                buffer.put(b).put(g).put(r).put(a);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=3) {
                buffer.put(curLine[i+2]).put(curLine[i+1]).put(curLine[i]).put((byte)0xFF);
            }
        }
    }

    private void copyRGBAtoABGR(ByteBuffer buffer, byte[] curLine) {
        for(int i=1,n=curLine.length ; i<n ; i+=4) {
            buffer.put(curLine[i+3]).put(curLine[i+2]).put(curLine[i+1]).put(curLine[i]);
        }
    }

    private void copyRGBAtoBGRA(ByteBuffer buffer, byte[] curLine) {
        for(int i=1,n=curLine.length ; i<n ; i+=4) {
            buffer.put(curLine[i+2]).put(curLine[i+1]).put(curLine[i]).put(curLine[i+3]);
        }
    }

    private void copyRGBAtoRGB(ByteBuffer buffer, byte[] curLine) {
        for(int i=1,n=curLine.length ; i<n ; i+=4) {
            buffer.put(curLine[i]).put(curLine[i+1]).put(curLine[i+2]);
        }
    }

    private void copyPALtoABGR(ByteBuffer buffer, byte[] curLine) {
        if(paletteA != null) {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = paletteA[idx];
                buffer.put(a).put(b).put(g).put(r);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = (byte)0xFF;
                buffer.put(a).put(b).put(g).put(r);
            }
        }
    }

    private void copyPALtoRGBA(ByteBuffer buffer, byte[] curLine) {
        if(paletteA != null) {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = paletteA[idx];
                buffer.put(r).put(g).put(b).put(a);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = (byte)0xFF;
                buffer.put(r).put(g).put(b).put(a);
            }
        }
    }

    private void copyPALtoBGRA(ByteBuffer buffer, byte[] curLine) {
        if(paletteA != null) {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = paletteA[idx];
                buffer.put(b).put(g).put(r).put(a);
            }
        } else {
            for(int i=1,n=curLine.length ; i<n ; i+=1) {
                int idx = curLine[i] & 255;
                byte r = palette[idx*3 + 0];
                byte g = palette[idx*3 + 1];
                byte b = palette[idx*3 + 2];
                byte a = (byte)0xFF;
                buffer.put(b).put(g).put(r).put(a);
            }
        }
    }

    private void expand4(byte[] src, byte[] dst) {
        for(int i=1,n=dst.length ; i<n ; i+=2) {
            int val = src[1 + (i >> 1)] & 255;
            switch(n-i) {
                default: dst[i+1] = (byte)(val & 15);
                case 1:  dst[i  ] = (byte)(val >> 4);
            }
        }
    }

    private void expand2(byte[] src, byte[] dst) {
        for(int i=1,n=dst.length ; i<n ; i+=4) {
            int val = src[1 + (i >> 2)] & 255;
            switch(n-i) {
                default: dst[i+3] = (byte)((val     ) & 3);
                case 3:  dst[i+2] = (byte)((val >> 2) & 3);
                case 2:  dst[i+1] = (byte)((val >> 4) & 3);
                case 1:  dst[i  ] = (byte)((val >> 6)    );
            }
        }
    }

    private void expand1(byte[] src, byte[] dst) {
        for(int i=1,n=dst.length ; i<n ; i+=8) {
            int val = src[1 + (i >> 3)] & 255;
            switch(n-i) {
                default: dst[i+7] = (byte)((val     ) & 1);
                case 7:  dst[i+6] = (byte)((val >> 1) & 1);
                case 6:  dst[i+5] = (byte)((val >> 2) & 1);
                case 5:  dst[i+4] = (byte)((val >> 3) & 1);
                case 4:  dst[i+3] = (byte)((val >> 4) & 1);
                case 3:  dst[i+2] = (byte)((val >> 5) & 1);
                case 2:  dst[i+1] = (byte)((val >> 6) & 1);
                case 1:  dst[i  ] = (byte)((val >> 7)    );
            }
        }
    }

    private void unfilter(byte[] curLine, byte[] prevLine) throws IOException {
        switch (curLine[0]) {
            case 0: // none
                break;
            case 1:
                unfilterSub(curLine);
                break;
            case 2:
                unfilterUp(curLine, prevLine);
                break;
            case 3:
                unfilterAverage(curLine, prevLine);
                break;
            case 4:
                unfilterPaeth(curLine, prevLine);
                break;
            default:
                throw new IOException("invalide filter type in scanline: " + curLine[0]);
        }
    }

    private void unfilterSub(byte[] curLine) {
        final int bpp = this.bytesPerPixel;
        for(int i=bpp+1,n=curLine.length ; i<n ; ++i) {
            curLine[i] += curLine[i-bpp];
        }
    }

    private void unfilterUp(byte[] curLine, byte[] prevLine) {
        final int bpp = this.bytesPerPixel;
        for(int i=1,n=curLine.length ; i<n ; ++i) {
            curLine[i] += prevLine[i];
        }
    }

    private void unfilterAverage(byte[] curLine, byte[] prevLine) {
        final int bpp = this.bytesPerPixel;

        int i;
        for(i=1 ; i<=bpp ; ++i) {
            curLine[i] += (byte)((prevLine[i] & 0xFF) >>> 1);
        }
        for(int n=curLine.length ; i<n ; ++i) {
            curLine[i] += (byte)(((prevLine[i] & 0xFF) + (curLine[i - bpp] & 0xFF)) >>> 1);
        }
    }

    private void unfilterPaeth(byte[] curLine, byte[] prevLine) {
        final int bpp = this.bytesPerPixel;

        int i;
        for(i=1 ; i<=bpp ; ++i) {
            curLine[i] += prevLine[i];
        }
        for(int n=curLine.length ; i<n ; ++i) {
            int a = curLine[i - bpp] & 255;
            int b = prevLine[i] & 255;
            int c = prevLine[i - bpp] & 255;
            int p = a + b - c;
            int pa = p - a; if(pa < 0) pa = -pa;
            int pb = p - b; if(pb < 0) pb = -pb;
            int pc = p - c; if(pc < 0) pc = -pc;
            if(pa<=pb && pa<=pc)
                c = a;
            else if(pb<=pc)
                c = b;
            curLine[i] += (byte)c;
        }
    }

    private void readIHDR() throws IOException {
        checkChunkLength(13);
        readChunk(buffer, 0, 13);
        width = readInt(buffer, 0);
        height = readInt(buffer, 4);
        bitdepth = buffer[8] & 255;
        colorType = buffer[9] & 255;

        switch (colorType) {
            case COLOR_GREYSCALE:
                if(bitdepth != 8) {
                    throw new IOException("Unsupported bit depth: " + bitdepth);
                }
                bytesPerPixel = 1;
                break;
            case COLOR_GREYALPHA:
                if(bitdepth != 8) {
                    throw new IOException("Unsupported bit depth: " + bitdepth);
                }
                bytesPerPixel = 2;
                break;
            case COLOR_TRUECOLOR:
                if(bitdepth != 8) {
                    throw new IOException("Unsupported bit depth: " + bitdepth);
                }
                bytesPerPixel = 3;
                break;
            case COLOR_TRUEALPHA:
                if(bitdepth != 8) {
                    throw new IOException("Unsupported bit depth: " + bitdepth);
                }
                bytesPerPixel = 4;
                break;
            case COLOR_INDEXED:
                switch(bitdepth) {
                    case 8:
                    case 4:
                    case 2:
                    case 1:
                        bytesPerPixel = 1;
                        break;
                    default:
                        throw new IOException("Unsupported bit depth: " + bitdepth);
                }
                break;
            default:
                throw new IOException("unsupported color format: " + colorType);
        }

        if(buffer[10] != 0) {
            throw new IOException("unsupported compression method");
        }
        if(buffer[11] != 0) {
            throw new IOException("unsupported filtering method");
        }
        if(buffer[12] != 0) {
            throw new IOException("unsupported interlace method");
        }
    }

    private void readPLTE() throws IOException {
        int paletteEntries = chunkLength / 3;
        if(paletteEntries < 1 || paletteEntries > 256 || (chunkLength % 3) != 0) {
            throw new IOException("PLTE chunk has wrong length");
        }
        palette = new byte[paletteEntries*3];
        readChunk(palette, 0, palette.length);
    }

    private void readtRNS() throws IOException {
        switch (colorType) {
            case COLOR_GREYSCALE:
                checkChunkLength(2);
                transPixel = new byte[2];
                readChunk(transPixel, 0, 2);
                break;
            case COLOR_TRUECOLOR:
                checkChunkLength(6);
                transPixel = new byte[6];
                readChunk(transPixel, 0, 6);
                break;
            case COLOR_INDEXED:
                if(palette == null) {
                    throw new IOException("tRNS chunk without PLTE chunk");
                }
                paletteA = new byte[palette.length/3];
                Arrays.fill(paletteA, (byte)0xFF);
                readChunk(paletteA, 0, paletteA.length);
                break;
            default:
                // just ignore it
        }
    }

    private void closeChunk() throws IOException {
        if(chunkRemaining > 0) {
            // just skip the rest and the CRC
            skip(chunkRemaining + 4);
        } else {
            readFully(buffer, 0, 4);
            int expectedCrc = readInt(buffer, 0);
            int computedCrc = (int)crc.getValue();
            if(computedCrc != expectedCrc) {
                throw new IOException("Invalid CRC");
            }
        }
        chunkRemaining = 0;
        chunkLength = 0;
        chunkType = 0;
    }

    private void openChunk() throws IOException {
        readFully(buffer, 0, 8);
        chunkLength = readInt(buffer, 0);
        chunkType = readInt(buffer, 4);
        chunkRemaining = chunkLength;
        crc.reset();
        crc.update(buffer, 4, 4);   // only chunkType
    }

    private void openChunk(int expected) throws IOException {
        openChunk();
        if(chunkType != expected) {
            throw new IOException("Expected chunk: " + Integer.toHexString(expected));
        }
    }

    private void checkChunkLength(int expected) throws IOException {
        if(chunkLength != expected) {
            throw new IOException("Chunk has wrong size");
        }
    }

    private int readChunk(byte[] buffer, int offset, int length) throws IOException {
        if(length > chunkRemaining) {
            length = chunkRemaining;
        }
        readFully(buffer, offset, length);
        crc.update(buffer, offset, length);
        chunkRemaining -= length;
        return length;
    }

    private void refillInflater(Inflater inflater) throws IOException {
        while(chunkRemaining == 0) {
            closeChunk();
            openChunk(IDAT);
        }
        int read = readChunk(buffer, 0, buffer.length);
        inflater.setInput(buffer, 0, read);
    }

    private void readChunkUnzip(Inflater inflater, byte[] buffer, int offset, int length) throws IOException {
        assert(buffer != this.buffer);
        try {
            do {
                int read = inflater.inflate(buffer, offset, length);
                if(read <= 0) {
                    if(inflater.finished()) {
                        throw new EOFException();
                    }
                    if(inflater.needsInput()) {
                        refillInflater(inflater);
                    } else {
                        throw new IOException("Can't inflate " + length + " bytes");
                    }
                } else {
                    offset += read;
                    length -= read;
                }
            } while(length > 0);
        } catch (DataFormatException ex) {
            throw (IOException)(new IOException("inflate error").initCause(ex));
        }
    }

    private void readFully(byte[] buffer, int offset, int length) throws IOException {
        do {
            int read = input.read(buffer, offset, length);
            if(read < 0) {
                throw new EOFException();
            }
            offset += read;
            length -= read;
        } while(length > 0);
    }

    private int readInt(byte[] buffer, int offset) {
        return
                ((buffer[offset  ]      ) << 24) |
                        ((buffer[offset+1] & 255) << 16) |
                        ((buffer[offset+2] & 255) <<  8) |
                        ((buffer[offset+3] & 255)      );
    }

    private void skip(long amount) throws IOException {
        while(amount > 0) {
            long skipped = input.skip(amount);
            if(skipped < 0) {
                throw new EOFException();
            }
            amount -= skipped;
        }
    }

    private static boolean checkSignature(byte[] buffer) {
        for(int i=0 ; i<SIGNATURE.length ; i++) {
            if(buffer[i] != SIGNATURE[i]) {
                return false;
            }
        }
        return true;
    }
}
